[Prizm] - Astuces pour optimiser vos add-ins !
Posté le 30/08/2014 22:22
Je crée ici un petit topic qui pourra recenser quelques astuces permettant d'optimiser vos programmes, notamment au niveau de l'aspect graphique (l'affichage, le dessin...) et de la gestion de la mémoire. Je publie ceci à l'occasion du concours pour que les participants sur Prizm puissent en profiter ;). Ce sera d'abord un peu en vrac, mais après, au fur et à mesure de vos suggestions et de vos astuces, le topic pourra évoluer vers quelque chose de plus intéressant :).
Comme je suis du genre à parler pas mal pour dire pas grand chose et donc me perdre un peu dans mes explications, je mets un petit résumé après chaque astuce ;).
Astuce 1 : Utilisation de (Load/Save)VRAM_1
Tout d'abord, dans un jeu du style de Zelda, ou tout autre jeu utilisant un gros Tileset, sur Prizm, le scrolling n'est pas souvent retenu par les programmeurs (car assez lent). Il est alors possible de profiter de ceci pour limiter le temps de réécriture du "fond" (c'est à dire des tiles restant fixes durant tout le temps où vous êtes sur le même tableau) et ne procédant à l'écriture dans la VRAM qu'une seule fois. En effet, la fonction SaveVRAM_1(); permet de sauvegarder toute la VRAM dans une zone de la mémoire qui n'est de toute manière pas utilisable autrement, et avec sa fonction complémentaire LoadVRAM_1(); de restaurer la VRAM. La phrase "écriture dans la VRAM une seule fois" n'est pas totalement vraie en fait, mais c'est une écriture en théorie plus rapide, car la plupart du temps, les sprites sont codés en 8 bits, ce qui implique de multiples accès en mémoire pour remplir la VRAM, tandis qu'avec (Save/Load)VRAM_1(); le transfert doit se résumer à une copie d'un unique tableau (ou du moins je suppose).
Le gain de FPS n'est pas énorme, et pas forcément très visible, mais cette méthode ne présente pas d'inconvénients majeurs à ma connaissance.
[u][b]Petit résumé[/b][/u] :
[b]→ Fonctions utilisées[/b] : void SaveVRAM_1() et void LoadVRAM_1(); disponible dans "display.h" pour le sdk standard
[b]→ Fonctionnement global[/b] : appeler SaveVRAM_1() après avoir dessiné une fois son fond, puis LoadVRAM_1() à chaque frame.
[b]→ Gain de fps[/b] : Assez léger.
[b]→ Avantages[/b] : Transfert plus rapide à priori, possibilité de libérer les tiles utilisés pour le fond après avoir dessiné une fois au chargement du tableau.
[b]→ Désavantages[/b] : Je n'en vois pas, si ce n'est une utilité limitée dès lors qu'il y a du scrolling.
Astuce 2 : Utilisation de Bdisp_PutDisp_DD_stripe
En "temps normal", lorsque le syscall Bdisp_PutDisp_DD est utilisé, c'est l'intégralité de la VRAM qui est transférée vers l'écran. Cela peut être nécessaire lorsque tout le contenu de l'écran est modifié d'une frame à l'autre, mais globalement (encore une fois cette technique est plus utilisable dans un jeu sans scrolling), d'une frame à l'autre, seule une portion de l'écran est modifiée. C'est là que le syscall Bdisp_PutDisp_DD_stripe rentre en jeu, il prend en argument deux entier y1 et y2 (avec y1 <= y2) qui correspondent aux lignes délimitant l'espace à rafraichir. La partie la plus "complexe" (même si ça ne l'est pas énormément
), vient du calcul de ces valeurs, mais ça, ça dépend de l'organisation et du fonctionnement de votre programme.
Petit exemple : Imaginons que je sache que mon écran doit être modifié de la ligne 50 à 82, ainsi que de la ligne 200 à 215, au lieu d'appeler :
Bdisp_PutDisp_DD();
j’appellerai plutôt
Bdisp_PutDisp_DD_stripe(50, 82);
Bdisp_PutDisp_DD_stripe(200, 215);
.
Le gain de fps peut être assez important (on peut monter à plus de 20 fps (~100 lignes) (voire jusqu'à 25(~40 lignes)) sans Overclock, contre 15 si on rafraichit tout l'écran).
Globalement, même si cela rajoute quelques calculs, je suis presque sur qu'il y aura tout le temps un gain.
Il y a néanmoins quelques inconvénients, le premier est que la fonction n'est pas toujours fonctionnelle sur l'émulateur (donc à implémenter juste sur les versions "finales" ou à tester sur la calculatrice elle même), la seconde est que cela entraine un framerate moins stable que si on rafraichit l'écran entièrement (même si il restera globalement supérieur), ce qui nécessitera pour un rendu plus propre d'adapter les déplacements en fonction du temps et non du fps (c'est à dire calculer un vecteur vitesse en fonction du nombre de ticks écoulé depuis le dernier appel, ou quelque chose du même acabit).
[b][u]Petit résumé[/u][/b] :
[b]→ Fonction utilisée[/b] : void Bdisp_PutDisp_DD_stripe(int y1, int y2); disponible dans "display_syscalls.h" pour le sdk standard
[b]→ Fonctionnement global[/b] : appeler Bdisp_PutDisp_DD_stripe afin de rafraichir uniquement la partie désirée de l'écran.
[b]→ Gain de fps[/b] : De nul si c'est mal géré (:E) à important.
[b]→ Avantages[/b] : Framerate amélioré suivant l'efficacité du calcul des coordonnées et de la situation en jeu.
[b]→ Désavantages[/b] : Non fonctionnel sur émulateur (nécessite de tester sur une vraie machine pour les calculs et autres, et de conserver Bdisp_PutDisp_DD() pour l'émulateur), framerate plus instable (nécessite un codage plus propre des déplacements / animations que la simple dépendance d'une boucle).
Partie 2 : Utilisation de la mémoire
1 : Petite introduction
Tout d'abord ce que je dis dans cette intro là est une sorte de compilation de ce que j'ai compris d'après diverses sources, il est possible que je ne sois pas totalement exact dans les mots utilisés, ou dans le fond (genre grosse connerie
), si vous avez des corrections ou précisions, je prends avec plaisir ;).
Nous allons parler ici des "3 mémoires" utilisés couramment quand on programme en C, j'ai nommé la
heap, la
mémoire statique et la
stack.
Commençons par parler de la
heap : c'est la zone de la RAM qui est dédiée à
l'allocation dynamique, c'est à dire que tous les appel de malloc, calloc ou encore realloc pointeront dans cette zone. Sur la Prizm, cette zone s'étend sur environ 128ko. Néanmoins la heap n'est pas des plus fiable et peut avoir certains comportements étranges dus à une mauvaise gestion par l'OS sans doute.
La durée de vie d'une variable dans la heap est contrôlée par le programmeur qui utilise à bon escient malloc et free ;).
Viennent ensuite la mémoire statique et la stack, qui en réalité se partagent une même partie de la RAM, occupant un peu plus de 500ko (524ko pour être plus précis). La mémoire statique est située au début de la zone, et la stack à la fin, cette dernière, et "grossissant vers le bas".
La mémoire statique est la mémoire dans laquelle sont définies les variables statiques et globales. Globalement, cette mémoire est assez peu utilisée quand le code est propre (à quelques exceptions près) (c'est à dire pas de gros tableau déclaré en global, ou pas 3k variables globales). La durée de vie d'une variable dans la mémoire statique est celle de l’exécution du programme.
La stack est une mémoire "provisoire" où se trouvent notamment les variables locales (pour ce qui nous intéresse ici). La durée de vie dans la stack est celle du bloc dans lequel est déclaré la variable (fonction...).
Petit exemple :
unsigned [purple]char[/purple] s[10]; // s pointe sur une zone de 10 octets réservée à la compilation dans la mémoire statique, zone qui sera réservée jusqu'à la fin de l’exécution.
[purple]int[/purple] main()
{
[purple]unsigned char[/purple] a = [gray]'c'[/gray]; // a est déclaré dans la stack et sa durée de vie est celle de la fonction main
unsigned char* tab = malloc(sizeof(unsigned char)*5); // tab pointe sur une zone réservée de 5 octets réservée pendant l’exécution dans la heap
s[0] = a;
function(tab)
free(tab);
}
void function(unsigned char* ptr)
{
[b][blue]if[/blue][/b](s[0] == [gray]'c'[/gray]) {
[purple]int[/purple] b = [maroon]4[/maroon]; // b est déclarée dans la stack et sa durée de vie est celle du bloc "if"
*ptr = b;
}
[b][blue]else[/blue][/b] {
[purple]int[/purple] b = [maroon]8[/maroon];
*ptr = b;
}
}
C'est pas forcément très propre, j'ai même pas testé le code, c'est juste pour illustrer ce que j'ai dit plus haut.
Ça peut paraitre un peu inutile de savoir ça, mais c'est assez important à mes yeux de comprendre comment la mémoire est occupée pour mieux la gérer.
Donc pour récapituler, on a la
heap de 128ko que l'on occupe avec malloc (et dérivés), la mémoire statique que l'on occupe avec nos variables globales et statiques, et la stack que l'on occupe temporairement avec des variables locales (avec stack + statique ayant à disponibilité un peu plus de 500ko).
2 : memmgr
Globalement, la mémoire la plus utilisée pour stocker des données (car c'est plus propre, plus flexible (on peut libérer quand on veut) c'est la
heap, mais sur Prizm, elle n'est pas très grande (en comparaison de la stack + mémoire statique), voire même un peu buggé (pas toujours de manière visible, mais avec des comportements internes étranges).
Je vais vous présenter une petite bibliothèque qui m'a l'air fort sympathique ici, j'ai nommé memmgr. Derrière ce nom un peu barbare, se cache de quoi faire de l'allocation dynamique dans la mémoire statique !
Alors, c'est un peu mensonger, car en réalité, la bibliothèque va se réserver un bloc de mémoire dans la mémoire statique, et ensuite va le gérer sans intervention de l'OS.
Je met un lien vers le site de l'auteur et la page où il présente la librairie :
http://eli.thegreenplace.net/2008/10/17/memmgr-a-fixed-pool-memory-allocator/
Le code est placé dans le domaine public donc pas de soucis à ce niveau là ! 8)
La librairie se compose de deux fichiers, memmgr.c et memmgr.h, qui sont à placer avec les sources de votre projet. S'intéresser au fichier .h suffit pour configurer la librairie : il faudra sans doute commenter
#define DEBUG_MEMMGR_SUPPORT_STATS 1
car le laisser provoquerait l'appel de fonctions non supportées de base (printf...).
Ensuite, vous avez :
#define POOL_SIZE 8 * 1024
#define MIN_POOL_ALLOC_QUANTAS 16
Ici, il est possible de définir la taille de votre "seconde heap" en changeant la "valeur" de POOL_SIZE (Je pense que monter à 128*1024 (donc 128kio) est convenable). Suivant la valeur que vous mettez, il faudra modifier le linker script (le fichier prizm.ld dans common pour le sdk de base) et changer la taille de la zone mémoire ram. Certains conseillent de le monter à 512ko, personnellement, je l'ai mis à 256k et ça me suffit
(je suppose que utiliser 500 ko de mémoire statique pose des problèmes de chevauchement avec la stack en fait
).
ram (rwx) : o = 0x08100004, l = 256k
En ce qui concerne
MIN_POOL_ALLOC_QUANTAS, son rôle est expliqué dans les commentaires, si vous ne comprenez pas, je vous conseille de ne pas y toucher, sinon, c'est à vous de voir en fonction de ce que vous allouez ;).
Une fois ces valeurs configurées, il suffit d'appeler memmgr_init() au début de l'exécution, puis d'utiliser memmgr_alloc et memmgr_free de la même manière que vous utilisiez malloc et free, vous avez donc deux heap de 128ko 8) (en considérant que votre consommation de mémoire statique est limitée à la base, si ce n'est pas le cas songez à réduire la valeur de POOL_SIZE).
Petite méthode pour connaitre la mémoire statique utilisée
Cliquer pour enrouler
Si jamais vous voulez connaître la mémoire statique utilisée par votre programme, qui est déterminée à la compilation, il suffit de compiler en ayant commenté la première ligne du linker script (dont on parle plus haut), c'est à dire :
/*OUTPUT_FORMAT(binary)*/
afin de générer quelque chose au format elf (même si le fichier porte toujours l'extension .bin, mais ça c'est dû au fonctionnement du SDK). Il faut ensuite lancer la commande
sh3eb-elf-objdump -hr addin.bin
où sh3eb-elf-objdump est le fichier du même nom avec l'extension .exe dans le dossier 'bin' du SDK et addin.bin le fichier .bin généré.
La quantité de mémoire statique utilisée est la somme des tailles (qui sont en hexadécimal) des sections .bss et .data (.bss contenant les variables non initialisées (pas de valeur donnée à la déclaration) et .data, celles initialisées).
Si vous êtes un peu curieux relancez la commande ci dessus avec -t au lieu de -hr pour avoir un aperçu bien complet de tous les symboles de votre addin.
N'oubliez pas de décommenter la ligne dans le linker script avant de recompiler pour mettre dans votre calculette !
Attention toutefois à ne pas mélanger vos pointeurs, les allocations avec malloc se libèrent avec free, et les allocations avec memmgr_alloc se libèrent avec memmgr_free !
D'ailleurs je suis preneur de retour sur cette bibliothèque, n'ayant fait que quelques petits tests jusqu'alors ;).
====================
J'ai encore quelques idées après, mais qui sont un peu plus lourdes et pas trop testées, donc j'en reparlerai un peu plus tard ^^.
Si vous avez des suggestions, des choses pour compléter la partie avantages/inconvénients, ou même vos propres astuces, n'hésitez pas à en faire part !
Citer : Posté le 31/08/2014 00:26 | #
J'ai rajouté la partie sur la mémoire, ou du moins ce que je voulais faire ce soir ;). Au final je me rend compte que ça ressemble plus à un cours (si l'on peut dire ) sur la mémoire qu'à une simple astuce d'optimisation, mais ça me semble important compte tenu qu'on a pas 4go de mémoire vive de bien comprendre (je ne sais pas si ce que j'ai écris permet de bien comprendre d'ailleurs ) comment ça se passe pour bien gérer et optimiser la consommation en mémoire vive (et comprendre l'intérêt de memmgr), je ne sais pas ce que vous en dites, je le laisse là où bien... ?
Je finirai ma relecture demain (enfin aujourd'hui techniquement mais bon x) ), je suis un peu crevé... Moi qui voulait avancer le projet pour le concours ce soir :whistle:...
Au passage, pas moyen de retrouver le bbcode pour faire une sorte de sommaire, ou du moins des références avec des liens.
Citer : Posté le 31/08/2014 00:33 | #
Excellent tutoriel qui contient plein d'informations utiles
Citer : Posté le 31/08/2014 01:35 | #
Je connaissais pas memmrg, je retiens
Ajouté le 31/08/2014 à 01:36 :
Ajouté à la liste des tutos de qualité !
Ajouté le 31/08/2014 à 01:40 :
Pour les liens, il me semble que c'est [ label = xxx ] (sans balise fermante), je cherche pour mettre un lien
Ajouté le 31/08/2014 à 01:42 :
Joli quadruple post, mais pour le lien, c'est [ target = xxx ] Lien [ /target ]
Citer : Posté le 31/08/2014 07:49 | #
C'est sympa dis donc
Par ailleurs je comprends avec l'allocation des locales dans la stack plusieurs morceaux de code Assembleur générés par le sh3eb-elf que j'avais étudiés mais pas compris.